﻿using EasyCaching.Core;
using Library.Core.Application;
using Library.Core.Application.IInfrastructure;
using Library.Core.Domain.Jwt;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;

namespace Library.Core.Infrastructure.Common
{
    public class JwtFactory : IJwtFactory
    {
        private readonly JwtIssuerOptions _jwtOptions;
        private readonly IRedisCachingProvider _redisCaching;

        public JwtSecurityToken Token { get; set; }


        public JwtFactory(IOptions<JwtIssuerOptions> jwtOptions
            , IRedisCachingProvider redisCaching)
        {
            _jwtOptions = jwtOptions.Value;
            ThrowIfInvalidOptions(_jwtOptions);

            _redisCaching = redisCaching;
        }

        /// <summary>
        /// 验证
        /// </summary>
        /// <param name="options"></param>
        private static void ThrowIfInvalidOptions(JwtIssuerOptions options)
        {
            if (options == null) throw new ArgumentNullException(nameof(options));

            if (options.ValidFor <= TimeSpan.Zero)
            {
                throw new ArgumentException("必须是非零时间跨度", nameof(JwtIssuerOptions.ValidFor));
            }
            if (options.SigningCredentials == null)
            {
                throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials));
            }

            if (options.JtiGenerator == null)
            {
                throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator));
            }
        }

        /// <summary>
        /// 生成Token
        /// </summary>
        /// <param name="loginInfo"></param>
        /// <returns></returns>
        public async Task<JwtLoginInfo> GenerateTokenAsync(JwtUserInfo loginInfo)
        {
            //为保证多个端可以同时登陆，所以从AudienceList中获取是否已经在其他端登陆过了
            var audienceDic = await _redisCaching.HMGetAsync("AudienceList", new List<string>() { loginInfo.UserName });
            //每个用户有其特定的audience
            if (audienceDic == null || (audienceDic != null && audienceDic[loginInfo.UserName] == null))
            {
                audienceDic[loginInfo.UserName] = Guid.NewGuid().ToString();
                await _redisCaching.HMSetAsync("AudienceList", audienceDic, _jwtOptions.ValidFor);
            }

            var claimsIdentity = new ClaimsIdentity(new GenericIdentity(loginInfo.UserName, "Token"));
            claimsIdentity.AddClaim(new Claim("Id", loginInfo.Id.ToString()));
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, loginInfo.UserName));

            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Sub, loginInfo.UserName),
                new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
                new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
                claimsIdentity.FindFirst(ClaimTypes.Name),
                claimsIdentity.FindFirst("Id")
            };
            claims.AddRange(claimsIdentity.FindAll(ClaimTypes.Role));

            //创建JWT安全令牌并对其进行编码
            var jwt = new JwtSecurityToken(
                            issuer: _jwtOptions.Issuer,
                            audience: audienceDic[loginInfo.UserName],
                            claims: claims,
                            notBefore: _jwtOptions.NotBefore,
                            expires: _jwtOptions.Expiration,
                            signingCredentials: _jwtOptions.SigningCredentials);
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);


            var jwtRefreshToken = new JwtSecurityToken(
                                      _jwtOptions.RefreshIssuer,
                                      audienceDic[loginInfo.UserName],
                                      claims,
                                      expires: _jwtOptions.RefreshExpiration,
                                      signingCredentials: _jwtOptions.SigningCredentials);
            var refreshToken = new JwtSecurityTokenHandler().WriteToken(jwtRefreshToken);

            var response = new JwtLoginInfo
            {
                AuthToken = encodedJwt,
                RefreshToken = refreshToken,
                ExpiresIn = (int)_jwtOptions.ValidFor.TotalSeconds,
                TokenType = "Bearer"
            };
            return response;
        }

        /// <summary>
        /// 检查刷新token的有效性
        /// </summary>
        /// <param name="refreshtoken"></param>
        /// <param name="accessToken"></param>
        /// <returns></returns>
        public async Task<JwtSecurityToken> CheckRefreshTokenAsync(string refreshtoken, string accessToken)
        {
            var jsth = new JwtSecurityTokenHandler();
            if (!jsth.CanReadToken(refreshtoken) || !jsth.CanReadToken(accessToken))
            {
                throw new ApplicationsException("Token无效！");
            }
            var tokenA = jsth.ReadJwtToken(accessToken);
            if (await TokenIsInInvalid(tokenA))
            {
                throw new ApplicationsException("该访问Token无效！");
            }
            var tokenR = jsth.ReadJwtToken(refreshtoken);
            var tokenExp = tokenR.Payload.Exp.Value;
            var expTime = StampToDateTime(tokenExp.ToString());
            if (expTime <= DateTime.Now)
            {
                throw new ApplicationsException("该刷新Token已过期！");
            }
            //检查两个token的Id和audience
            if (!tokenR.Subject.Equals(tokenA.Subject) || !tokenR.Payload.Aud.First().Equals(tokenA.Payload.Aud.First()))
            {
                throw new ApplicationsException("该刷新Token无效！");
            }
            return tokenA;
        }

        /// <summary>
        /// 验证token有效性
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public async Task<bool> TokenIsInInvalid(JwtSecurityToken? token)
        {
            var tokenInfo = token != null ? token : Token;

            //redis中是否存在该token的无效key
            if (await _redisCaching.KeyExistsAsync($"InvaildToken-{tokenInfo.RawData}"))
            {
                return true;
            }
            var key = tokenInfo.Claims.SingleOrDefault(x => x.Type.Equals("sub")).Value;
            var fields = new List<string>() { key };
            var userAudience = await _redisCaching.HMGetAsync("AudienceList", fields);
            //audience不存在或不相等的话表明用户已经修改密码，或者权限已更新。
            if (userAudience == null || (userAudience != null && userAudience[key] == null))
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// 加入黑名单
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public async Task AddTokenToBlackList(JwtSecurityToken token)
        {
            var tokenExp = token.Payload.Exp.Value;
            var time = StampToDateTime(tokenExp.ToString());
            if (time > DateTime.Now)
            {
                var val = time - DateTime.Now;
                //将当前Token打上无效token的标签放到redis中，并在一定时间后清除这个key
                await _redisCaching.StringSetAsync($"InvaildToken-{token.RawData}", "", val);
            }
        }

        /// <summary>
        /// 删除授权中对应用户信息
        /// </summary>
        /// <param name="userNameList"></param>
        /// <returns></returns>
        public async Task MakeUserAllTokenInvalid(List<string> userNameList)
        {
            if (userNameList?.Count > 0)
            {
                var res = await _redisCaching.HDelAsync("AudienceList", userNameList);
            }
        }

        /// <summary>
        /// 从Unix纪元（1970年1月1日，UTC午夜）转换为秒的日期。
        /// </summary>
        /// <param name="date"></param>
        /// <returns></returns>
        private static long ToUnixEpochDate(DateTime date)
        {
            return (long)Math.Round((date.ToUniversalTime() -
                              new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero))
                             .TotalSeconds);
        }

        /// <summary>
        /// 时间戳转时间
        /// </summary>
        /// <param name="timeStamp"></param>
        /// <returns></returns>
        private DateTime StampToDateTime(string timeStamp)
        {
            DateTime dateTimeStart = TimeZoneInfo.ConvertTime(new DateTime(1970, 1, 1), TimeZoneInfo.Utc, TimeZoneInfo.Local);
            long lTime = long.Parse(timeStamp + "0000000");
            TimeSpan toNow = new TimeSpan(lTime);
            return dateTimeStart.Add(toNow);
        }
    }
}
